﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using Faml.Optimization;
using System.Diagnostics;

namespace Faml.Optimization
{
    public class MergeTable
    {
        private Dictionary<string, Dictionary<string,List<OptimizationEntry>>> table;
        private int pairCount = 0;

        public MergeTable()
        {
            table = new Dictionary<string, Dictionary<string, List<OptimizationEntry>>>();
        }

        public int OptimizationsCount
        {
            get { return table.Count; }
        }

        public int AddOptimizationPairs(Assembly assembly)
        {
            Type[] types = assembly.GetExportedTypes();
            int pairsAdded = 0;
            foreach(Type type in types) 
                if (type.IsClass && type.GetCustomAttributes(typeof(OptimizationContainerAttribute), false).Length > 0) 
                    pairsAdded += AddOptimizationPairs(type);
            return pairsAdded;
        }

        public int AddOptimizationPairs(Type container)
        {
            int pairsAdded = 0;
            if (container.IsClass && container.GetCustomAttributes(typeof(OptimizationContainerAttribute), false).Length > 0)
            {
                MethodInfo[] methods = container.GetMethods();
                foreach(MethodInfo mi in methods)
                    pairsAdded += AddOptimizationPair(mi);
            }
            return pairsAdded;
        }

        public int AddOptimizationPair(MethodInfo optimizationMethod)
        {
            object[] attrs = optimizationMethod.GetCustomAttributes(typeof(OptimizationForPairAttribute), true);
            if (attrs.Length == 0)
                return 0;

            OptimizationForPairAttribute op = (OptimizationForPairAttribute)attrs[0];
            OptimizationEntry entry = new OptimizationEntry(op, optimizationMethod);

 
            Dictionary<string, List<OptimizationEntry>> innerDict;
            if (!table.TryGetValue(op.OuterOperationName, out innerDict))
            {
                innerDict = new Dictionary<string, List<OptimizationEntry>>();
                table.Add(op.OuterOperationName, innerDict);
            }
            List<OptimizationEntry> list; 
            if (!innerDict.TryGetValue(op.InnerOperationName, out list))
            {
                list = new List<OptimizationEntry>();
                innerDict.Add(op.InnerOperationName, list);
            }

            list.Add(entry);
            return 1; // One pair is added
        }

        /// <summary>Looks up for merge optimization for given pair of methods</summary>
        /// <param name="opA">First method</param>
        /// <param name="opB">Second method</param>
        /// <returns>Merge result or null of no optimization is found</returns>
        public MethodInfo GetOptimizationForPair(MethodInfo opA, MethodInfo opB)
        {
            Dictionary<string,List<OptimizationEntry>> innerDict = null;
            if (table.TryGetValue(opA.Name, out innerDict))
            {
                List<OptimizationEntry> list = null;
                if (innerDict.TryGetValue(opB.Name, out list))
                    foreach (OptimizationEntry optimization in list)            
                        return optimization.GetOptimization(opA, opB);
            }
            return null;
        }

        private struct OptimizationEntry
        {
            OptimizationForPairAttribute op;
            MethodInfo optimized;

            public OptimizationEntry(OptimizationForPairAttribute op, MethodInfo optimized)
            {
                this.op = op;
                this.optimized = optimized;
            }

            public MethodInfo GetOptimization(MethodInfo operationA, MethodInfo operationB)
            {
#if DEBUG
                if (string.Compare(operationA.Name, op.OuterOperationName, true) != 0 ||
                    string.Compare(operationB.Name, op.InnerOperationName, true) != 0)
                    throw new Exception("This optimization is not suitable for proposed operations");

                if(operationA.IsGenericMethodDefinition || operationB.IsGenericMethodDefinition)
                    throw new Exception("Operations cannot be generic definitions");
#endif
                
                ParameterInfo[] pars = optimized.GetParameters();
                ParameterInfo[] parsA = operationA.GetParameters();
                ParameterInfo[] parsB = operationB.GetParameters();

                int innerArgCount = pars.Length - op.OuterArgumentsCount + 1;
                if (innerArgCount != parsB.Length || op.OuterArgumentsCount != parsA.Length)
                    throw new Exception("This optimization is not suitable for proposed operations");

                if (parsA[op.IndexOfInnerOperation].ParameterType != operationB.ReturnType)
                    throw new Exception("This optimization is not suitable for proposed operations");
                
                Type[] typesC = new Type[pars.Length];

                int i;
                for (i = 0; i < op.IndexOfInnerOperation; i++)
                {
                    typesC[i] = parsA[i].ParameterType;
                }

                for (int j = 0; j < innerArgCount; i++, j++)
                {
                    typesC[i] = parsB[j].ParameterType;
                }

                for (int j = op.IndexOfInnerOperation + 1; i < pars.Length; i++, j++)
                {
                    typesC[i] = parsA[j].ParameterType;
                }

                MethodInfo operationC;
                if (optimized.IsGenericMethod)
                {
                    TypeMatcher typeMatcher = new TypeMatcher(optimized.GetGenericArguments());
                    if (!typeMatcher.Match(pars, typesC))
                        throw new Exception("This optimization is not suitable for proposed operations");

                    operationC = optimized.MakeGenericMethod(typeMatcher.ActualTypes);
                    pars = operationC.GetParameters();
                }
                else
                {
                    operationC = optimized;
                }

                if (operationC.ReturnType != optimized.ReturnType)
                    return null; // Optimization's return type differs from the original operation's return type

                for (int u = 0; u < pars.Length; u++)
                {
                    if (pars[u].ParameterType != typesC[u])
                        return null; //  Parameters do not match
                }

                return operationC;
            }
        }
    }



    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public class OptimizationContainerAttribute : Attribute
    {
        public OptimizationContainerAttribute()
        {
        }
    }

    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public class OptimizationForPairAttribute : Attribute
    {
        /// <summary>
        /// Describes merge optimization, when two operations A and B 
        /// (and B is an argument of A)
        /// can be efficiently evaluated by a single operation C.
        /// Such operation C must have this attribute.
        /// </summary>
        /// <param name="operationA">Name of the outer operation.</param>
        /// <param name="operationB">Name of the inner operation.</param>
        /// <param name="argCountA">Total number of arguments of outer operation (including inner operation).</param>
        /// <param name="argBPosition">Index of an argument of outer operation which is an inner operation.</param>
        public OptimizationForPairAttribute(string operationA, string operationB, int argCountA, int argBPosition)
        {
            this.opA = operationA;
            this.opB = operationB;
            this.argBPos = argBPosition;
            this.argCountA = argCountA;
        }

        private int argCountA;

        public int OuterArgumentsCount
        {
            get { return argCountA; }
            set { argCountA = value; }
        }

        private int argBPos;

        public int IndexOfInnerOperation
        {
            get { return argBPos; }
            set { argBPos = value; }
        }

        private string opA;

        public string OuterOperationName
        {
            get { return opA; }
            set { opA = value; }
        }

        private string opB;

        public string InnerOperationName
        {
            get { return opB; }
            set { opB = value; }
        }


    }
}
